from math import sin, cos, pi, exp, log, e
from tokenizer import tokenize, is_num
import copy

# all evals will return float object

PRIORITY = {
    '**': 3,
    '*': 2,
    '/': 2,
    '+': 1,
    '-': 1,
    '$': 0
}


OPERATIONS = {
    '**': lambda x, y: x**y,
    '*': lambda x, y: x*y,
    '/': lambda x, y: x/y,
    '+': lambda x, y: x+y,
    '-': lambda x, y: x-y,
}

FUNCTIONS = {
    'sin': sin,
    'cos': cos,
    'exp': exp,
    'log': log,
}


CONSTANT = {
    'pi': 3.14,
    'e': 2.718
}


def eval_expr(tokens):
    func = False
    brackets = []
    i = 0
    while i < len(tokens):
        if tokens[i] in CONSTANT.keys():
            tokens[i] = str(CONSTANT[tokens[i]])
        if tokens[i] in FUNCTIONS.keys():
            func = True
        if tokens[i] in {'('}:
            brackets.append(i)
        elif tokens[i] in {')'}:
            try:
                if not func:
                    result = str(eval_single_expr(tokens[brackets[-1] + 1: i]))
                    tokens.insert(i + 1, result)
                    del tokens[brackets[-1]: i + 1]
                    i = brackets.pop()  # reset i point to the result
                else:
                    result = str(eval_function(tokens[brackets[-1] - 1: i + 1]))
                    tokens.insert(i + 1, result)
                    del tokens[brackets[-1] - 1: i + 1]
                    i = brackets.pop() - 1  # reset i point to the result
                    func = False
            except:
                raise RuntimeError('ERROR_UNMATCHED_BRACKETS')
        i += 1
    if len(brackets) != 0:
        raise RuntimeError('ERROR_UNMATCHED_BRACKETS')
    return eval_single_expr(tokens)


def eval_function(tokens):
    # func(x, y, ...)
    func = FUNCTIONS[tokens[0]]
    args = []
    start = 2  # arg start from tokens[2]
    try:
        i = 0
        while i < len(tokens):
            if tokens[i] in {','}:
                args.append(tokens[start:i])
                start = i + 1
            i += 1
        args.append(tokens[start:-1])  # last arg: ", arg)"
        argv = []
        for arg in args:
            argv.append(eval_expr(arg))
        return func(*argv)
    except:
        raise RuntimeError('ERROR_WRONG_SYNTAX')


def eval_single_expr(tokens):
    operators = []
    operands = []
    # get negative signal
    i = 1
    while i < len(tokens):
        if is_num(tokens[i], signed=False) and tokens[i-1] in {'-', '+'}:
            if tokens[i - 2] in OPERATIONS.keys() or i == 1:
                tokens[i] = tokens[i-1] + tokens[i]
                tokens.pop(i-1)
                i -= 1
        i += 1
    # get operators and operands
    required = 0  # There must be an operand after an operator and expr must start with an operand
    i = 0
    while i < len(tokens):
        if is_num(tokens[i], signed=True):
            if required != 0:
                raise RuntimeError('ERROR_WRONG_EXPRESSION')
            operands.append(float(tokens[i]))
            required = 1  # require operator
        elif tokens[i] in OPERATIONS:
            if required != 1:
                raise RuntimeError('ERROR_WRONG_EXPRESSION')
            operators.append(tokens[i])
            required = 0  # require operand
        i += 1
    if len(operands) != len(operators) + 1:
        raise RuntimeError('ERROR_WRONG_EXPRESSION')
    i = len(operators) - 1
    # handle operation power
    while i >= 0:
        if operators[i] in {'**'}:
            operator = operators.pop(i)
            operand1 = operands.pop(i)
            operand2 = operands.pop(i)
            result = OPERATIONS[operator](operand1, operand2)
            operands.insert(i, result)
            i += 1
        i -= 1
    # handle operation multi and div
    i = 0
    while i < len(operators):
        if operators[i] in {'*', '/'}:
            operator = operators.pop(i)
            operand1 = operands.pop(i)
            operand2 = operands.pop(i)
            result = OPERATIONS[operator](operand1, operand2)
            operands.insert(i, result)
            i -= 1  # operator has been pop
        i += 1
    # handle operation add and sub
    i = 0
    while i < len(operators):
        if operators[i] in {'+', '-'}:
            operator = operators.pop(i)
            operand1 = operands.pop(i)
            operand2 = operands.pop(i)
            result = OPERATIONS[operator](operand1, operand2)
            operands.insert(i, result)
            i -= 1
        i += 1
    return operands[0]


def subs(expr_argu, token, val):
    expr = copy.deepcopy(expr_argu)
    i = 0
    while i < len(expr_argu):
        if expr[i] == token:
            expr[i] = str(val)
        i += 1
    return eval_expr(expr)

if __name__ == '__main__':
    src = 'sin(3.14 / 2)'
    tokens = tokenize(src)
    result = eval_expr(tokens)
    print(result)